#! user/bin/python
# -*- coding: utf-8 -*-

"""
Created on 2021/8/18 14:17

@File: 一个跨平台的python程序启动框架_python_launchmodes.py
@Author: LT
@Description: 它实现了一个抽象的超类LaunchMode，后者定义了用shell命令行开始一个Python程序的内容，但不定义其实现方式，
              而是由它的子类来提供一个run方法。这个方法实际上负责根据一个给定的方案启动一个Python程序并（可选地）定义了
              一个announce方法以在启动时显示程序的名称↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓


              用命令行和可复用的启动方案类来启动Python程序；在命令行开头自动向Python可执行文件插入
              "python"和/或路径；这个模块的某些部分可能假定'python'在你的系统路径中；

              使用subprocess模块模块也可行，不过os.popen()在内部调用这个模块，目标是在这里启动一个独立运行的程序，而非连接到它的流；
              multiprocessing模块也是一个选择，不过这里处理命令行而非函数，为实现这里的选项之一而开始一个进程不是很合理；

              这一版的更新：脚本文件名路径将经过normpath()处理，必要时将所有'/'改成'\'；以供Windows工具使用；
              PyEdit和其他工具继承这个修正；在Windows下，一般允许在文件打开中用/，但并非所有启动工具；

              !!!!!!!!!!它不需要再在命令行中敲入python py文件名 参数了，省去了python，直接——py文件名 参数——就行了，我比作者多考虑了一步!!!!!!!!!!!
"""
import sys
import os

pyPATH = sys.executable                                             # 指的是执行python的绝对路径
pyFILE = sys.platform[:3] == 'win' and 'python.exe' or 'python'     # 这句语法糖相当于 'python.exe' if sys.platform[:3] == 'win' else 'python'


def fixWindowsPath(cmdline) -> str:
    """
    这个cmdline是不用包含python打头的，已经由系统指定了，这样做好处在于任何装了python的电脑下不用设定环境变量
    将cmdline开头的脚本文件名路径里的'/'改成'\'，然后将'\'变成'\\'；
    在Windows下，仅为运行需要这种处理的工具的类所使用；
    在其他平台上，这么做也没有坏处（比如Unix下的os.system）；
    :param cmdline:
    :return:
    """
    splitline = cmdline.lstrip().split(' ')                         # 在空格处分割字符串
    if splitline[0].lower().endswith("python") or splitline[0].lower().endswith("python.exe"):
        # 这是为了应对这种之类的输入->：.\..\PyThON.Exe'
        fixedPATH = os.path.normpath(splitline[1])                  # 既然0号写成了python，那么1号元素就是被执行文件了
        return ' '.join([fixedPATH] + splitline[2:])                # 把路径重新拼起来
    fixedPATH = os.path.normpath(splitline[0])                      # 解决斜杠的问题
    return ' '.join([fixedPATH] + splitline[1:])                    # 把路径重新拼起来


class LaunchMode:
    """
    在实例中待命，声明标签并运行命令；子类按照run()中的需要格式化命令行；
    """
    def __init__(self, label, command):
        self.what = label
        self.where = command

    def __call__(self):                                             # 等待调用，当在实例后面加()时会被触发
        self.announce(self.what)
        self.run(self.where)                                        # 子类必须定义run()

    def announce(self, text):                                       # 子类可以重新定义announce()
        print(text)                                                 # 用方法代替if/elif逻辑

    def run(self, cmdline):
        assert False, 'run must be defined'


class System(LaunchMode):
    """
    运行shell命令行中指定的Python脚本；小心：可能阻塞调用者，除非在
    Unix下带上&操作符
    """
    def run(self, cmdline):
        cmdline = fixWindowsPath(cmdline)
        os.system('%s %s' % (pyPATH, cmdline))


class Popen(LaunchMode):
    """
    在新进程中运行shell命令行；小心：可能阻塞调用者，因为管道关闭得太快
    """
    def run(self, cmdline):
        cmdline = fixWindowsPath(cmdline)
        os.popen(pyPATH + ' ' + cmdline)                            # 假设没有数据可读取


class Fork(LaunchMode):
    """
    在显示地创建的新进程中运行命令，仅在类Unix系统下可用，包括Cygwin
    """
    def run(self, cmdline):
        assert hasattr(os, 'fork'), 'platform is win os does not have attr fork'
        cmdline = cmdline.split()                                   # 把字符串转换成列表
        if os.fork() == 0:                                          # 开始新的子进程
            os.execvp(pyPATH, [pyFILE] + cmdline)                   # 在子进程中运行新程序


class Start(LaunchMode):
    """
    独立于调用者运行程序；仅在Windows下可用：使用了文件名关联
    """
    def run(self, cmdline):
        assert sys.platform[:3] == 'win', 'platform is not win cant use start cmdline'
        cmdline = fixWindowsPath(cmdline)
        os.startfile(cmdline)


class StartArgs(LaunchMode):
    """
    仅在Windows下可用：args可能需要用到真正的start命令；斜杠在这里没有问题
    """
    def run(self, cmdline):
        assert sys.platform[:3] == 'win', 'platform is not win cant use start cmdline'
        os.system('start ' + cmdline)                               # 可能会创建弹出窗口


class Spawn(LaunchMode):
    """
    在独立于调用者的新进程中运行Python；在Windows和Unix下都可用；
    Dos中使用P_NOWAIT；斜杠在这里没有问题
    """
    def run(self, cmdline):
        os.spawnv(os.P_DETACH, pyPATH, (pyFILE, cmdline))


# 为这个平台挑选一个“最佳”启动器
# 可能需要在其他平台上细化这个选项

if sys.platform[:3] == 'win':
    PortableLauncher = Spawn
else:
    PortableLauncher = Fork


class QuietPortableLauncher(PortableLauncher):
    def announce(self, text):
        pass


def self_test():
    file = 'echo.py'                                                # <------请指定实际存在的.py文件
    input('default mode...')
    launcher = PortableLauncher(file, file)
    launcher()                                                      # 不阻塞；会触发__call__，从而调用run()

    input('system mode...')
    System(file, file)()                                            # 阻塞

    if sys.platform[:3] == 'win':
        input('DOS start mode...')                                  # 不阻塞
        StartArgs(file, file)()


if __name__ == '__main__':
    self_test()
